﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;

namespace UnitOfWork
{
    /// <summary>
    /// Implementation of IUnitOfWork interface. Methods is not described here because all of it is described
    /// in IUnitOfWork
    /// </summary>
    /// <typeparam name="TDbContext"></typeparam>
    public class EFUnitOfWork<TDbContext> : IUnitOfWork where TDbContext : DbContext
    {
        private IDictionary<Type, object> repositories = new Dictionary<Type, object>();
        private DbContext efContext;
        private bool disposed = false;

        public bool IsConnectionOpen { get { return efContext != null; } }
             
        public void Insert<TEntity>(TEntity entity) where TEntity : class
        {
            InitializeEFContext();
            
            GetRepo<TEntity>().Insert(entity);
        }

        public void Update<TEntity>(TEntity entity) where TEntity : class
        {
            InitializeEFContext();

            GetRepo<TEntity>().Update(entity);
        }

        public TEntity GetById<TEntity>(params object[] ids) where TEntity : class
        {
            InitializeEFContext();

            return GetRepo<TEntity>().GetById(ids);
        }

        public IQueryable<TEntity> Get<TEntity>() where TEntity : class
        {
            InitializeEFContext();

            return GetRepo<TEntity>().Get();
        }

        public IQueryable<TEntity> Get<TEntity>(System.Linq.Expressions.Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null) where TEntity : class
        {
            InitializeEFContext();

            return GetRepo<TEntity>().Get(filter, orderBy);
        }

        public IQueryable<TResult> Join<TEntityOuter, TEntityInner, TResult>(Func<TEntityOuter, object> outerKeySelector, Func<TEntityInner, object> innerKeySelector, Func<TEntityOuter, TEntityInner, TResult> resultSelector)
            where TEntityOuter : class
            where TEntityInner : class
        {
            InitializeEFContext();

            return GetRepo<TEntityOuter>().Get().Join(GetRepo<TEntityInner>().Get(), outerKeySelector, innerKeySelector, resultSelector).AsQueryable();
        }

        public IQueryable<TResult> Join<TEntityOuter, TEntityInner, TResult>(Func<TEntityOuter, object> outerKeySelector, Func<TEntityInner, object> innerKeySelector, Func<TEntityOuter, TEntityInner, TResult> resultSelector, IEqualityComparer<object> comparer)
            where TEntityOuter : class
            where TEntityInner : class
        {
            InitializeEFContext();

            return GetRepo<TEntityOuter>().Get().Join(GetRepo<TEntityInner>().Get(), outerKeySelector, innerKeySelector, resultSelector, comparer).AsQueryable();
        }

        public IQueryable<TResult> LeftJoin<TEntityOuter, TEntityInner, TResult>(Func<TEntityOuter, object> outerKeySelector, Func<TEntityInner, object> innerKeySelector, Func<TEntityOuter, TEntityInner, TResult> resultSelector)
            where TEntityOuter : class
            where TEntityInner : class
        {
            InitializeEFContext();

            return GetRepo<TEntityOuter>().Get().GroupJoin(GetRepo<TEntityInner>().Get(), outerKeySelector, innerKeySelector, (p, q) => resultSelector(p, q.FirstOrDefault())).AsQueryable();
        }

        public IQueryable<TResult> LeftJoin<TEntityOuter, TEntityInner, TResult>(Func<TEntityOuter, object> outerKeySelector, Func<TEntityInner, object> innerKeySelector, Func<TEntityOuter, TEntityInner, TResult> resultSelector, IEqualityComparer<object> comparer)
            where TEntityOuter : class
            where TEntityInner : class
        {
            InitializeEFContext();

            return GetRepo<TEntityOuter>().Get().GroupJoin(GetRepo<TEntityInner>().Get(), outerKeySelector, innerKeySelector, (p, q) => resultSelector(p, q.FirstOrDefault()), comparer).AsQueryable();
        }

        public void DeleteById<TEntity>(params object[] ids) where TEntity : class
        {
            InitializeEFContext();

            GetRepo<TEntity>().DeleteById(ids);
        }

        public void Delete<TEntity>(TEntity entity) where TEntity : class
        {
            InitializeEFContext();

            GetRepo<TEntity>().Delete(entity);
        }

        public void Query(Action query)
        {
            InitializeEFContext();

            query.Invoke();

            SaveChanges(true);
        }

        public void SaveChanges(bool withDisposing = false)
        {
            efContext.SaveChanges();

            if (withDisposing)
            {
                Dispose();
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (efContext != null)
                    {
                        efContext.Dispose();
                        efContext = null;
                    }

                    repositories.Clear();
                }
            }
            this.disposed = true;
        }
        
        private void InitializeEFContext()
        {
            if (efContext == null)
            {
                efContext = typeof(TDbContext).GetConstructor(new Type[] { }).Invoke(new object[] { }) as DbContext;
            }            
        }

        private IRepository<TEntity> GetRepo<TEntity>() where TEntity : class
        {
            if (!repositories.ContainsKey(typeof(TEntity)))
            {
                repositories.Add(new KeyValuePair<Type, object>(typeof(TEntity), new EFRepository<TEntity>(efContext)));
            }
            
            return (IRepository<TEntity>)repositories[typeof(TEntity)];            
        }
    }
}
